using System;

namespace PropertyExpression.Common
{
    public static class OptionalValue
    {
        public static IOptionalValue<TValue> NoValue<TValue>()
        {
            return NoValue<TValue>(unknownAdditionalInformationFunc);
        }

        public static IOptionalValue<TValue> NoValue<TValue>(Func<object> additionalInformationFunc)
        {
            return new NotExistOptionalValue<TValue>(additionalInformationFunc);
        }

        public static IOptionalValue<TValue> AsOptionalResult<TValue>(
            this TValue result)
        {
            return new ExistOptionalValue<TValue>(result);
        }

        public static IOptionalValue<TTarget> Execute<TSource, TTarget>(
            this IOptionalValue<TSource> optionalValue,
            Func<TSource, TTarget> func)
        {
            return optionalValue.Execute(
                t => func(t).AsOptionalResult(),
                NoValue<TTarget>
                );
        }

        public static IOptionalValue<TTarget> Execute<TSource, TTarget>(
            this IOptionalValue<TSource> optionalValue,
            Func<TSource, IOptionalValue<TTarget>> func)
        {
            return optionalValue.Execute(func, NoValue<TTarget>);
        }

        public static void Execute<TValue>(
            this IOptionalValue<TValue> optionalValue, 
            Action<TValue> existAction, 
            Action notExistAction)
        {
            optionalValue.Execute(existAction.ToFunc(), notExistAction.ToFunc());
        }

        public static void ExecuteIfExist<TValue>(
            this IOptionalValue<TValue> optionalValue, 
            Action<TValue> existAction)
        {
            Execute(optionalValue, existAction, () => { });
        }

        public static TValue GetValueOrDefault<TValue>(
            this IOptionalValue<TValue> optionalValue)
        {
            return optionalValue.Execute(result => result, () => default(TValue));
        }

        public static TResult Execute<TValue, TResult>(
            this IOptionalValue<TValue> optionalValue,
            Func<TValue, TResult> existFunc, Func<TResult> notExistFunc)
        {
            if (optionalValue.HasValue)
            {
                return existFunc(optionalValue.Value);
            }
            else
            {
                return notExistFunc();
            }
        }

        private static readonly Func<object> unknownAdditionalInformationFunc = () => "unknown";

        private class NotExistOptionalValue<TValue> : IOptionalValue<TValue>
        {
            private readonly Func<object> innerAdditionalInformationFunc;

            public NotExistOptionalValue(Func<object> innerAdditionalInformationFunc)
            {
                this.innerAdditionalInformationFunc = innerAdditionalInformationFunc;
            }

            public TValue Value
            {
                get { return GetValue(innerAdditionalInformationFunc); }
            }

            public TValue GetValue(Func<object> additionalInformationFunc)
            {
                throw new InvalidOperationException(
                    string.Format("Optional value of '{0}' type has no value. Additional information '{1}'.",
                                  typeof(TValue),
                                  additionalInformationFunc()
                        )
                    );
            }

            public bool HasValue
            {
                get { return false; }
            }
        }

        private class ExistOptionalValue<TValue> : IOptionalValue<TValue>
        {
            private readonly TValue result;

            public ExistOptionalValue(TValue result)
            {
                this.result = result;
            }

            public TValue Value
            {
                get { return result; }
            }

            public TValue GetValue(Func<object> additionalInformationFunc)
            {
                return Value;
            }

            public bool HasValue
            {
                get { return true; }
            }
        }
    }
}